<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="gPchr" id="gPchr"><span data-lake-id="u965c013d" id="u965c013d">典型回答</span></h1>
  <p data-lake-id="ue56c9402" id="ue56c9402"><br></p>
  <p data-lake-id="u0a468f28" id="u0a468f28"><span data-lake-id="uc3665daf" id="uc3665daf">MVCC，是Multiversion Concurrency Control的缩写，翻译过来是多版本并发控制，和数据库锁一样，他也是一种并发控制的解决方案。</span></p>
  <p data-lake-id="u5688ecbb" id="u5688ecbb"><span data-lake-id="u3f82555f" id="u3f82555f">​</span><br></p>
  <p data-lake-id="uffb45ffb" id="uffb45ffb"><span data-lake-id="u7aef1b6c" id="u7aef1b6c">我们知道，在数据库中，对数据的操作主要有2种，分别是读和写，而在并发场景下，就可能出现以下三种情况：</span></p>
  <p data-lake-id="uc23dbbf8" id="uc23dbbf8"><span data-lake-id="ubdd303c9" id="ubdd303c9">​</span><br></p>
  <p data-lake-id="uecd09c62" id="uecd09c62"><span data-lake-id="u80f97dca" id="u80f97dca">读-读并发</span></p>
  <p data-lake-id="uc4c12d5b" id="uc4c12d5b"><span data-lake-id="u78ae1d7a" id="u78ae1d7a">读-写并发</span></p>
  <p data-lake-id="ufa0d1440" id="ufa0d1440"><span data-lake-id="u20932f47" id="u20932f47">写-写并发</span></p>
  <p data-lake-id="uc1abe452" id="uc1abe452"><span data-lake-id="u94fb78e7" id="u94fb78e7">​</span><br></p>
  <p data-lake-id="u53ff6b3f" id="u53ff6b3f"><span data-lake-id="u8eea7113" id="u8eea7113">我们都知道，在没有写的情况下读-读并发是不会出现问题的，而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么，读-写并发则可以通过MVCC的机制解决。</span></p>
  <p data-lake-id="u832091e1" id="u832091e1"><span data-lake-id="u40a45f5c" id="u40a45f5c">​</span><br></p>
  <h1 data-lake-id="fAxo3" id="fAxo3"><span data-lake-id="u883afec3" id="u883afec3">扩展知识</span></h1>
  <h3 data-lake-id="718fbcad" id="718fbcad"><span data-lake-id="ue4d86d22" id="ue4d86d22">快照读和当前读</span></h3>
  <p data-lake-id="u510b9505" id="u510b9505"><span data-lake-id="u8c6620dc" id="u8c6620dc">要想搞清楚MVCC的机制，最重要的一个概念那就是快照读。</span></p>
  <p data-lake-id="ub644c49c" id="ub644c49c"><br></p>
  <p data-lake-id="u5481a590" id="u5481a590"><span data-lake-id="u823b4a2b" id="u823b4a2b">所谓快照读，就是读取的是快照数据，即快照生成的那一刻的数据，像我们常用的普通的SELECT语句在不加锁情况下就是快照读。如：</span></p>
  <p data-lake-id="u1d0ba8bb" id="u1d0ba8bb"><br></p>
  <pre lang="java"><code>
SELECT * FROM xx_table WHERE ...
</code></pre>
  <p data-lake-id="ueb898db8" id="ueb898db8"><br></p>
  <p data-lake-id="u872f22e7" id="u872f22e7"><span data-lake-id="ue4008df4" id="ue4008df4">和快照读相对应的另外一个概念叫做当前读，当前读就是读取最新数据，所以，加锁的 SELECT，或者对数据进行增删改都会进行当前读，比如：</span></p>
  <p data-lake-id="ua463079a" id="ua463079a"><br></p>
  <pre lang="java"><code>
SELECT * FROM xx_table LOCK IN SHARE MODE;

SELECT * FROM xx_table FOR UPDATE;

INSERT INTO xx_table ...

DELETE FROM xx_table ...

UPDATE xx_table ...
</code></pre>
  <p data-lake-id="uf52dd2c8" id="uf52dd2c8"><br></p>
  <p data-lake-id="u5dd5bfc1" id="u5dd5bfc1"><span data-lake-id="u30a76134" id="u30a76134">可以说</span><strong><span data-lake-id="uf4055d0b" id="uf4055d0b">快照读是MVCC实现的基础</span></strong><span data-lake-id="u5d38e7e8" id="u5d38e7e8">，而</span><strong><span data-lake-id="u2b186ba7" id="u2b186ba7">当前读是悲观锁实现的基础</span></strong><span data-lake-id="u23b21413" id="u23b21413">。</span></p>
  <p data-lake-id="u97d459d4" id="u97d459d4"><br></p>
  <p data-lake-id="u850b408a" id="u850b408a"><span data-lake-id="u6aa003fe" id="u6aa003fe">那么，快照读读到的快照是从哪里读到的的呢？换句话说，快照是存在哪里的呢？</span></p>
  <p data-lake-id="uf47489e3" id="uf47489e3"><br></p>
  <h3 data-lake-id="UndoLog" id="UndoLog"><span data-lake-id="u640371a8" id="u640371a8">UndoLog</span></h3>
  <p data-lake-id="u5a805ed4" id="u5a805ed4"><br></p>
  <p data-lake-id="u35e9e7f8" id="u35e9e7f8"><span data-lake-id="ucdcc5090" id="ucdcc5090">undo log是Mysql中比较重要的事务日志之一，顾名思义，undo log是一种用于回退的日志，在事务没提交之前，MySQL会先记录更新前的数据到 undo log日志文件里面，当事务回滚时或者数据库崩溃时，可以利用 undo log来进行回退。</span></p>
  <p data-lake-id="ub6cfbbcb" id="ub6cfbbcb"><br></p>
  <p data-lake-id="u5a348894" id="u5a348894"><span data-lake-id="u6f7fef36" id="u6f7fef36">这里面提到的存在undo log中的"更新前的数据"就是我们前面提到的快照。所以，这也是为什么很多人说UndoLog是MVCC实现的重要手段的原因。</span></p>
  <p data-lake-id="u180c5dd0" id="u180c5dd0"><br></p>
  <p data-lake-id="u84c5aaee" id="u84c5aaee"><span data-lake-id="uac3c249f" id="uac3c249f">那么，一条记录在同一时刻可能有多个事务在执行，那么，undo log会有一条记录的多个快照，那么在这一时刻发生SELECT要进行快照读的时候，要读哪个快照呢？</span></p>
  <p data-lake-id="u56889d03" id="u56889d03"><br></p>
  <p data-lake-id="u12a764d4" id="u12a764d4"><span data-lake-id="u1edbb1f4" id="u1edbb1f4">这就需要用到另外几个信息了。</span></p>
  <p data-lake-id="u4340b925" id="u4340b925"><br></p>
  <h3 data-lake-id="046ad6ae" id="046ad6ae"><span data-lake-id="uc3c18429" id="uc3c18429">行记录的隐式字段</span></h3>
  <p data-lake-id="u39086cd9" id="u39086cd9"><br></p>
  <p data-lake-id="u943c0c86" id="u943c0c86"><span data-lake-id="u357550c3" id="u357550c3">其实，数据库中的每行记录中，除了保存了我们自己定义的一些字段以外，还有一些重要的隐式字段的：</span></p>
  <p data-lake-id="u9207a7f6" id="u9207a7f6"><br></p>
  <ul list="uf9a63c52">
   <li fid="u1c3bf90d" data-lake-id="u6c1bc19b" id="u6c1bc19b"><span data-lake-id="ufa372b57" id="ufa372b57"> db_row_id：隐藏主键，如果我们没有给这个表创建主键，那么会以这个字段来创建聚簇索引。 </span></li>
   <li fid="u1c3bf90d" data-lake-id="ueb45d758" id="ueb45d758"><span data-lake-id="u054306a3" id="u054306a3"> db_trx_id：对这条记录做了最新一次修改的事务的ID </span></li>
   <li fid="u1c3bf90d" data-lake-id="u03983bda" id="u03983bda"><span data-lake-id="u3f6932ba" id="u3f6932ba"> db_roll_ptr：回滚指针，指向这条记录的上一个版本，其实他指向的就是Undo Log中的上一个版本的快照的地址。 </span></li>
  </ul>
  <p data-lake-id="u966c9a83" id="u966c9a83"><br></p>
  <p data-lake-id="u6cb95dc9" id="u6cb95dc9"><span data-lake-id="u70f3b0b5" id="u70f3b0b5">因为每一次记录变更之前都会先存储一份快照到undo log中，那么这几个隐式字段也会跟着记录一起保存在undo log中，就这样，每一个快照中都有一个db_trx_id字段表示了对这个记录做了最新一次修改的事务的ID ，以及一个db_roll_ptr字段指向了上一个快照的地址。（db_trx_id和db_roll_ptr是重点，后面还会用到）</span></p>
  <p data-lake-id="uf916692f" id="uf916692f"><br></p>
  <p data-lake-id="u88185bb2" id="u88185bb2"><span data-lake-id="ucdcfe468" id="ucdcfe468">这样，就形成了一个快照链表：</span></p>
  <p data-lake-id="uf3d69a97" id="uf3d69a97"><br></p>
  <p data-lake-id="u4eb789c4" id="u4eb789c4"><img src="https://www.hollischuang.com/wp-content/uploads/2022/09/16640978319826.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_48%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="uaa768c84" id="uaa768c84"><br></p>
  <p data-lake-id="u6187d071" id="u6187d071"><span data-lake-id="ua285ac94" id="ua285ac94">有了undo log，又有了几个隐式字段，我们好像还是不知道具体应该读取哪个快照，那怎么办呢？</span></p>
  <p data-lake-id="ueabe97b0" id="ueabe97b0"><br></p>
  <h3 data-lake-id="5f023112" id="5f023112"><span data-lake-id="uacc4f1ed" id="uacc4f1ed">Read View</span></h3>
  <p data-lake-id="u6cf3b857" id="u6cf3b857"><br></p>
  <p data-lake-id="u7c6f2969" id="u7c6f2969"><span data-lake-id="u327bef32" id="u327bef32">这时候就需要Read View 登场了，</span></p>
  <p data-lake-id="u210527a7" id="u210527a7"><br></p>
  <p data-lake-id="ucd0ebef0" id="ucd0ebef0"><span data-lake-id="u5561aa47" id="u5561aa47">Read View 主要来帮我们解决可见性的问题的, 即他会来告诉我们本次事务应该看到哪个快照，不应该看到哪个快照。</span></p>
  <p data-lake-id="uca036513" id="uca036513"><br></p>
  <p data-lake-id="u44d23897" id="u44d23897"><span data-lake-id="u570c3677" id="u570c3677">在 Read View 中有几个重要的属性：</span></p>
  <p data-lake-id="u63392541" id="u63392541"><br></p>
  <ul list="uc3d958c7">
   <li fid="ufa58da19" data-lake-id="u389f4c79" id="u389f4c79"><span data-lake-id="u173902e4" id="u173902e4">trx_ids，系统当前未提交的事务 ID 的列表。</span></li>
   <li fid="ufa58da19" data-lake-id="u64d94242" id="u64d94242"><span data-lake-id="u9c84913a" id="u9c84913a">low_limit_id，应该分配给下一个事务的id 值。</span></li>
   <li fid="ufa58da19" data-lake-id="u8cad9ce5" id="u8cad9ce5"><span data-lake-id="u40f7ed14" id="u40f7ed14">up_limit_id，未提交的事务中最小的事务 ID。</span></li>
   <li fid="ufa58da19" data-lake-id="u96335873" id="u96335873"><span data-lake-id="ub37d7acd" id="ub37d7acd">creator_trx_id，创建这个 Read View 的事务 ID。</span></li>
  </ul>
  <p data-lake-id="uba9d6bea" id="uba9d6bea"><br></p>
  <blockquote data-lake-id="u75fdbf6e" id="u75fdbf6e">
   <p data-lake-id="u5008ae20" id="u5008ae20"><span data-lake-id="u6700fbb9" id="u6700fbb9">每开启一个事务，我们都会从数据库中获得一个事务 ID，这个事务 ID 是自增长的，通过 ID 大小，我们就可以判断事务的时间顺序。</span></p>
  </blockquote>
  <p data-lake-id="u9437ed8d" id="u9437ed8d"><br></p>
  <p data-lake-id="u2c3e3e20" id="u2c3e3e20"><span data-lake-id="ub79b1a0d" id="ub79b1a0d">那么，一个事务应该看到哪些快照，不应该看到哪些快照该如何判断呢？</span></p>
  <p data-lake-id="uad4db193" id="uad4db193"><br></p>
  <p data-lake-id="u186960a9" id="u186960a9"><span data-lake-id="u54c923f9" id="u54c923f9">其实原则比较简单，那就是事务ID大的事务应该能看到事务ID小的事务的变更结果，反之则不能！举个例子：</span></p>
  <p data-lake-id="ufa420ff2" id="ufa420ff2"><br></p>
  <p data-lake-id="u516bd809" id="u516bd809"><span data-lake-id="ub7324fa9" id="ub7324fa9">假如当前有一个事务3想要对某条记录进行一次快照读的时候，他会先创建一个Read View，并且把当前所有还未提交的事务的信息记录下来。比如up_limit_id = 2，low_limit_id = 5，trx_ids= [2,4,5]，creator_trx_id= 3</span></p>
  <p data-lake-id="uc8127310" id="uc8127310"><br></p>
  <p data-lake-id="uebb43a4c" id="uebb43a4c"><img src="https://www.hollischuang.com/wp-content/uploads/2022/09/16640979909632.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_38%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u27b3d3da" id="u27b3d3da"><br></p>
  <p data-lake-id="u40fdedc5" id="u40fdedc5"><span data-lake-id="ubcc5de2d" id="ubcc5de2d">我们前面说过，每一条记录上都有一个隐式字段db_trx_id记录对这条记录做了最新一次修改的事务的ID，如db_trx_id = 3；</span></p>
  <p data-lake-id="u9e814ea9" id="u9e814ea9"><br></p>
  <p data-lake-id="uc03e683a" id="uc03e683a"><span data-lake-id="u333999ac" id="u333999ac">那么接下来，数据库会拿这条记录db_trx_id和Read View进行可见性比较。</span></p>
  <p data-lake-id="u12e2e2d2" id="u12e2e2d2"><br></p>
  <p data-lake-id="u507e580c" id="u507e580c"><span data-lake-id="u8263db52" id="u8263db52">如果db_trx_id&lt;up_limit_id，则说明，在Read View中所有未提交的事务创建之前，db_trx_id = 3的这个事务就已经提交了，并且在这期间，并没有新的事务提交。所以，这条记录对当前事务就应该是可见的。</span></p>
  <p data-lake-id="uaccd7616" id="uaccd7616"><br></p>
  <p data-lake-id="u4d3ddcc4" id="u4d3ddcc4"><span data-lake-id="uf018bf7e" id="uf018bf7e">如果，db_trx_id&gt;low_limit_id，则说明，db_trx_id = 3的这个事务是在Read View中所有未提交的事务创建之后才提交的，也就是说，在当前事务开启之后，有别的事务修改了数据并作了提交。所以，这个记录对于当前事务来说应该就是不可见的。（不可见怎么办呢？后面讲）</span></p>
  <p data-lake-id="ub870c6b2" id="ub870c6b2"><br></p>
  <p data-lake-id="u53c52b76" id="u53c52b76"><span data-lake-id="u3f9573a5" id="u3f9573a5">那么，还有另外一种情况，那就是up_limit_id &lt; db_trx_id &lt; low_limit_id，这种情况下，会再拿db_trx_id和Read View中的trx_ids进行逐一比较。</span></p>
  <p data-lake-id="uc482bf0a" id="uc482bf0a"><br></p>
  <p data-lake-id="ubf150dab" id="ubf150dab"><span data-lake-id="u4b613081" id="u4b613081">如果，db_trx_id在trx_ids列表中，那么表示在当前事务开启时，并未提交的某个事务在修改数据之后提交了，那么这个记录对于当前事务来说应该是不可见的。</span></p>
  <p data-lake-id="u9e3c807f" id="u9e3c807f"><br></p>
  <p data-lake-id="u157f351b" id="u157f351b"><span data-lake-id="u18fa8e49" id="u18fa8e49">如果，db_trx_id不在trx_ids列表中，那么表示的是在当前事务开启之前，其他事务对数据进行修改并提交了，所以，这条记录对当前事务就应该是可见的。</span></p>
  <p data-lake-id="u003ee8e5" id="u003ee8e5"><br></p>
  <p data-lake-id="u313058be" id="u313058be"><span data-lake-id="uf1c35c54" id="uf1c35c54">所以，当读取一条记录的时候，经过以上判断，发现记录对当前事务可见，那么就直接返回就行了。那么如果不可见怎么办？没错，那就需要用到undo log了。</span></p>
  <p data-lake-id="u161dc006" id="u161dc006"><br></p>
  <p data-lake-id="uff4c4cf9" id="uff4c4cf9"><span data-lake-id="ubdf8a6bb" id="ubdf8a6bb">当数据的事务ID不符合Read View规则时候，那就需要从undo log里面获取数据的历史快照，然后数据快照的事务ID再来和Read View进行可见性比较，如果找到一条快照，则返回，找不到则返回空。</span></p>
  <p data-lake-id="uf4047a11" id="uf4047a11"><br></p>
  <p data-lake-id="ufd0b3d1c" id="ufd0b3d1c"><img src="https://www.hollischuang.com/wp-content/uploads/2022/09/16640994166058.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_47%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="uac8166f1" id="uac8166f1"><br></p>
  <p data-lake-id="ued9b597d" id="ued9b597d"><span data-lake-id="u8d549bea" id="u8d549bea">所以，总结一下，在InnoDB中，MVCC就是通过Read View + Undo Log来实现的，undo log中保存了历史快照，而Read View 用来判断具体哪一个快照是可见的。</span></p>
  <p data-lake-id="ua12c0e91" id="ua12c0e91"><br></p>
  <h3 data-lake-id="c566ff9c" id="c566ff9c"><span data-lake-id="uf73cf283" id="uf73cf283">MVCC和可重复读</span></h3>
  <p data-lake-id="u97a6df10" id="u97a6df10"><br></p>
  <p data-lake-id="u0657875b" id="u0657875b"><span data-lake-id="u1836f654" id="u1836f654">其实，根据不同的事务隔离级别，Read View的获取时机是不同的，在RC下，一个事务中的每一次SELECT都会重新获取一次Read View，而在RR下，一个事务中只在第一次SELECT的时候会获取一次Read View。</span></p>
  <p data-lake-id="ucf7ce6fb" id="ucf7ce6fb"><br></p>
  <p data-lake-id="u02b73f71" id="u02b73f71"><span data-lake-id="u9e6cde91" id="u9e6cde91">所以，可重复读这种事务隔离级别之下，因为有MVCC机制，就可以解决不可重复读的问题，因为他只有在第一次SELECT的时候才会获取一次Read View，天然不存在重复读的问题了。</span></p>
 </body>
</html>